﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Windows.Forms;

namespace NExplorer.Controls
{
	internal sealed class ExplorerVirtualListView : ListView
	{
		#region ColumnWorkItem class

		private class ColumnWorkItem
		{
			public Guid DirectoryId;
			public FileSystemInfo FileSystemInfo;
			public ColumnProviderInfo ColumnProviderInfo;

			public ColumnWorkItem(ColumnProviderInfo columnProviderInfo, Guid directoryId, FileSystemInfo fileSystemInfo)
			{
				ColumnProviderInfo = columnProviderInfo;
				DirectoryId = directoryId;
				FileSystemInfo = fileSystemInfo;
			}
		}

		#endregion

		#region ExplorerListViewDefaultComparer

		private class ExplorerListViewComparer : IComparer<FileSystemListViewItem>, IComparer
		{
			private readonly IComparer _comparer;
			private readonly int _columnIndex;
			private readonly int _ascendingMultiplier;

			public ExplorerListViewComparer(int columnIndex, IComparer comparer, bool ascending)
			{
				_columnIndex = columnIndex;
				_comparer = comparer;
				_ascendingMultiplier = ascending ? 1 : -1;
			}

			public int Compare(FileSystemListViewItem x, FileSystemListViewItem y)
			{
				int result;

				// Adjust for Explorer-like folder priority
				if (x.IsDirectory && !y.IsDirectory)
				{
					result = -1;
				}
				else if (!x.IsDirectory && y.IsDirectory)
				{
					result = 1;
				}
				else
				{
					ExplorerListViewSubItem subItemX = x.SubItems[_columnIndex] as ExplorerListViewSubItem;

					if (subItemX == null)
					{
						result = _comparer.Compare(x.SubItems[_columnIndex].Text, y.SubItems[_columnIndex].Text);
					}
					else
					{
						ExplorerListViewSubItem subItemY = (ExplorerListViewSubItem)y.SubItems[_columnIndex];
						result = _comparer.Compare(subItemX.ColumnItemData.Data, subItemY.ColumnItemData.Data);
					}
				}

				return result * _ascendingMultiplier;
			}

			public int Compare(object x, object y)
			{
				return Compare((FileSystemListViewItem)x, (FileSystemListViewItem)y);
			}
		}

		#endregion

		public event EventHandler				ItemCountChanged;

		private readonly ExplorerService		_explorerService;
		private readonly ColumnProviderManager	_columnProviderManager;
		private readonly FileSystemWatcher		_fsWatcher = new FileSystemWatcher();
		private readonly List<IComparer>		_columnComparers = new List<IComparer>();
		private FileSystemListViewItem[]		_cachedListViewItems;
		private FileSystemInfo[]				_currentFileSystemInfos = new FileSystemInfo[0];
		private bool							_ignoreExplorerSelectionChanges = false;
		private bool							_ignoreUserSelectionChanges = false;
		private Guid							_currentDirectoryId;
		private int								_lastColumnSorted = 0;
		private bool							_lastColumnSortedAscending = true;

		public ExplorerVirtualListView()
		{
			DoubleBuffered = true;
			VirtualMode = true;

			_explorerService = (ExplorerService)ServiceManager.Explorer;
			_columnProviderManager = (ColumnProviderManager)ServiceManager.ColumnProviderManager;

			SmallImageList = new ImageList
			{
				ImageSize = new Size(16, 16),
				ColorDepth = ColorDepth.Depth32Bit
			};

			_fsWatcher.IncludeSubdirectories = false;
			_fsWatcher.Changed += HandleFileSystemChange;
			_fsWatcher.Deleted += HandleFileSystemChange;
			_fsWatcher.Renamed += HandleFileSystemChange;
			_fsWatcher.Created += HandleFileSystemChange;
		}

		protected override void OnHandleCreated(EventArgs e)
		{
			base.OnHandleCreated(e);
			Utilities.SetExplorerStyle(this);
		}

		protected override void InitLayout()
		{
			base.InitLayout();

			if (!DesignMode)
			{
				_explorerService.CurrentDirectoryChanged += delegate { Populate(); };
				_explorerService.ColumnItemUpdated += ColumnItemUpdated;
				_explorerService.RefreshRequested += delegate { Populate(); };

				_columnProviderManager.ColumnProviderAdded += delegate { Reset(); };
				_columnProviderManager.ColumnProviderRemoved += delegate { Reset(); };

				_explorerService.SelectedItemsChanged += WhenSelectedItemsChanged;

				// Create columns
				Reset();
			}
		}

		protected override void OnColumnClick(ColumnClickEventArgs e)
		{
			if (e.Column < Columns.Count)
			{
				bool ascending;

				if (_lastColumnSorted == e.Column)
				{
					ascending = !_lastColumnSortedAscending;
				}
				else
				{
					ascending = true;
				}

				_lastColumnSorted = e.Column;
				_lastColumnSortedAscending = ascending;

				Sort();
			}
		}

		private void ColumnItemUpdated(Guid directoryId, int columnId, FileSystemInfo fileSystemInfo, ColumnItemData columnItemData)
		{
			// Only handle calls if the directory is the current directory.
			if (directoryId == _currentDirectoryId)
			{
				if (InvokeRequired)
				{
					Invoke(new ColumnItemUpdatedHandler(ColumnItemUpdated), directoryId, columnId, fileSystemInfo, columnItemData);
				}
				else
				{
					int index = Array.FindIndex(_currentFileSystemInfos, info => info == fileSystemInfo);

					if (index > -1)
					{
						FileSystemListViewItem lvi = _cachedListViewItems[index];
						lvi.SubItems[columnId] = new ExplorerListViewSubItem(lvi, columnItemData);
						RedrawItems(index, index, false);
					}
				}
			}
		}

		protected override void OnMouseUp(MouseEventArgs e)
		{
			base.OnMouseUp(e);

			ListViewItem lvi = GetItemAt(e.X, e.Y);

			if (e.Button == MouseButtons.Left)
			{
				if (SelectedIndices.Count == 0 && lvi == null)
				{
					HandleUserSelectionChange();
				}
			}
			else if (e.Button == MouseButtons.Right)
			{
				ContextMenuStrip menu = _explorerService.CreateContextMenu();
				menu.Show(this, e.Location);
			}
		}

		private void OnItemCountChanged()
		{
			if (ItemCountChanged != null)
			{
				ItemCountChanged(this, EventArgs.Empty);
			}
		}

		protected override void OnRetrieveVirtualItem(RetrieveVirtualItemEventArgs e)
		{
			base.OnRetrieveVirtualItem(e);

			FileSystemListViewItem lvi = _cachedListViewItems[e.ItemIndex];

			if (lvi == null)
			{
				lvi = CreateAndCacheListViewItem(e.ItemIndex);
			}

			e.Item = lvi;
		}

		protected override void OnCacheVirtualItems(CacheVirtualItemsEventArgs e)
		{
			base.OnCacheVirtualItems(e);

			for (int i = e.StartIndex; i <= e.EndIndex; i++)
			{
				if (_cachedListViewItems[i] == null)
				{
					CreateAndCacheListViewItem(i);
				}
			}
		}

		private FileSystemListViewItem CreateAndCacheListViewItem(int i)
		{
			FileSystemListViewItem lvi = _cachedListViewItems[i];

			if (lvi == null)
			{
				FileSystemInfo fileSystemInfo = _currentFileSystemInfos[i];
				lvi = new FileSystemListViewItem(fileSystemInfo);
				lvi.Initialize(SmallImageList);

				foreach (ColumnProviderInfo columnProviderInfo in _columnProviderManager.ProviderInfos)
				{
					if (columnProviderInfo.Enabled)
					{
						ThreadPool.QueueUserWorkItem(ProcessWorkItem, new ColumnWorkItem(columnProviderInfo, _currentDirectoryId, fileSystemInfo));
					}
				}
			}

			_cachedListViewItems[i] = lvi;

			return lvi;
		}

		private static void ProcessWorkItem(object state)
		{
			ColumnWorkItem workItem = (ColumnWorkItem)state;
			IColumnProvider columnProvider = workItem.ColumnProviderInfo.ColumnProvider;

			if (workItem.ColumnProviderInfo.Enabled)
			{
				try
				{
					columnProvider.ProcessItem(workItem.DirectoryId, workItem.ColumnProviderInfo.ColumnIndex, workItem.FileSystemInfo);
				}
				catch (Exception e)
				{
					workItem.ColumnProviderInfo.Enabled = false;

					string error =
						string.Format("The following unhandled exception occurred in the '{0}' column provider. It has been disabled.\n\n{1}",
									  columnProvider.GetType().Name, e.Message);
					MessageBox.Show(error, "Column Provider Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
				}
			}
		}

		private void Reset()
		{
			Items.Clear();
			Columns.Clear();
			_columnComparers.Clear();

			Comparer comparer = new Comparer(CultureInfo.CurrentCulture);
			_columnComparers.Add(comparer); // Name
			_columnComparers.Add(comparer); // Date modified
			_columnComparers.Add(comparer); // Type
			_columnComparers.Add(comparer); // Size

			AddColumn("Name", 300);
			AddColumn("Date modified", 150);
			AddColumn("Type", 150);
			AddColumn("Size", 75, HorizontalAlignment.Right);

			foreach (ColumnProviderInfo providerInfo in _columnProviderManager.ProviderInfos)
			{
				IColumnProvider provider = providerInfo.ColumnProvider;
				_columnComparers.Add(provider.ItemComparer);
				AddColumn(provider.ColumnHeader, provider.DefaultWidth, provider.Alignment);
			}
		}

		// For all but multi-select with shift.
		protected override void OnItemSelectionChanged(ListViewItemSelectionChangedEventArgs e)
		{
			base.OnItemSelectionChanged(e);

			HandleUserSelectionChange();
		}

		// Multi-select with Shift.
		protected override void OnVirtualItemsSelectionRangeChanged(ListViewVirtualItemsSelectionRangeChangedEventArgs e)
		{
			base.OnVirtualItemsSelectionRangeChanged(e);

			if (e.IsSelected)
			{
				HandleUserSelectionChange();
			}
		}
		
		private void AddColumn(string text, int width)
		{
			AddColumn(text, width, HorizontalAlignment.Left);
		}

		private void AddColumn(string text, int width, HorizontalAlignment alignment)
		{
			Columns.Add(new ColumnHeader { Text = text, TextAlign = alignment, Width = width });
		}

		private void WhenSelectedItemsChanged(object sender, EventArgs e)
		{
			if (!_ignoreExplorerSelectionChanges)
			{
				if (InvokeRequired)
				{
					Invoke(new EventHandler(WhenSelectedItemsChanged), sender, e);
				}
				else
				{
					SelectItems();
				}
			}
		}

		private void SelectItems()
		{
			_ignoreUserSelectionChanges = true;
			BeginUpdate();

			SelectedIndices.Clear();

			foreach (FileSystemInfo fsi in ServiceManager.Explorer.SelectedItems)
			{
				// TODO: Speed this up
				for (int i = 0; i < _currentFileSystemInfos.Length; i++)
				{
					if (fsi.FullName == _currentFileSystemInfos[i].FullName)
					{
						SelectedIndices.Add(i);
						break;
					}
				}
			}

			EndUpdate();
			_ignoreUserSelectionChanges = false;
		}

		private void HandleUserSelectionChange()
		{
			if (!_ignoreUserSelectionChanges)
			{
				List<FileSystemInfo> infos = new List<FileSystemInfo>();

				foreach (int i in SelectedIndices)
				{
					infos.Add(_currentFileSystemInfos[i]);
				}

				// If no items are selected, select the folder.
				if (infos.Count == 0)
				{
					infos.Add(_explorerService.CurrentDirectory);
				}

				_ignoreExplorerSelectionChanges = true;
				_explorerService.SelectedItems = infos.ToArray();
				_ignoreExplorerSelectionChanges = false;
			}
		}

		private void Populate()
		{
			_currentDirectoryId = Guid.NewGuid();

			Items.Clear();
			SmallImageList.Images.Clear();
			SelectedIndices.Clear();

			DirectoryInfo directory = _explorerService.CurrentDirectory;
			
			_currentFileSystemInfos = directory.GetFileSystemInfos();
			_cachedListViewItems = new FileSystemListViewItem[_currentFileSystemInfos.Length];

			_fsWatcher.Path = directory.FullName;
			_fsWatcher.EnableRaisingEvents = true;

			VirtualListSize = _currentFileSystemInfos.Length;

			SelectItems();

			OnItemCountChanged();
		}

		private new void Sort()
		{
			for (int i = 0; i < _currentFileSystemInfos.Length; i++)
			{
				CreateAndCacheListViewItem(i);
			}

			Array.Sort(_cachedListViewItems, new ExplorerListViewComparer(_lastColumnSorted, _columnComparers[_lastColumnSorted], _lastColumnSortedAscending));

			for (int i = 0;  i < _cachedListViewItems.Length; i++)
			{
				_currentFileSystemInfos[i] = _cachedListViewItems[i].FileSystemInfo;
			}
				
			Refresh();
		}

		protected override void OnMouseDoubleClick(MouseEventArgs e)
		{
			base.OnMouseDoubleClick(e);

			NavigateToSelectedItem();
		}

		private void NavigateToSelectedItem()
		{
			if (SelectedIndices.Count == 1)
			{
				_explorerService.NavigateTo(_currentFileSystemInfos[SelectedIndices[0]]);
			}
		}

		protected override void OnSearchForVirtualItem(SearchForVirtualItemEventArgs e)
		{
			base.OnSearchForVirtualItem(e);

			Predicate<FileSystemInfo> findAction = fsi => fsi.Name.StartsWith(e.Text, StringComparison.InvariantCultureIgnoreCase);

			int index = Array.FindIndex(_currentFileSystemInfos, e.StartIndex, findAction);

			if (index == -1)
			{
				index = Array.FindIndex(_currentFileSystemInfos, 0, e.StartIndex, findAction);
			}

			e.Index = index;
		}

		private void HandleFileSystemChange(object sender, FileSystemEventArgs e)
		{
			if (InvokeRequired)
			{
				Invoke(new FileSystemEventHandler(HandleFileSystemChange), null, e);
			}
			else
			{
				switch (e.ChangeType)
				{
					case WatcherChangeTypes.Created:
					case WatcherChangeTypes.Deleted:
					case WatcherChangeTypes.Changed:
						{
							Populate();
							break;
						}
					case WatcherChangeTypes.Renamed:
						{
							Populate();
							Refresh(); // Items weren't always repainting
							break;
						}
				}
			}
		}
	}
}
